﻿namespace Neovolve.Toolkit.Reflection
{
    using System;
    using System.Reflection;
    using Neovolve.Toolkit.Storage;

    /// <summary>
    /// The <see cref="MethodResolver"/>
    ///   class resolves <see cref="MethodInfo"/>
    ///   instances of types and caches results for faster access.
    /// </summary>
    public static class MethodResolver
    {
        /// <summary>
        /// Stores the cache used to store <see cref="RuntimeMethodHandle"/> values.
        /// </summary>
        private static readonly ICacheStore _cacheStore = CacheStoreFactory.Create();

        /// <summary>
        /// Resolves the specified declaring type.
        /// </summary>
        /// <param name="declaringType">
        /// Type of the declaring.
        /// </param>
        /// <param name="methodName">
        /// Name of the method.
        /// </param>
        /// <param name="parameterTypes">
        /// The parameter types.
        /// </param>
        /// <returns>
        /// A <see cref="MethodInfo"/> instance.
        /// </returns>
        /// <exception cref="ArgumentNullException">
        /// The <paramref name="declaringType"/> is <c>null</c>.
        /// </exception>
        /// <exception cref="ArgumentNullException">
        /// The <paramref name="methodName"/> is <c>null</c> or equals <see cref="String.Empty"/>.
        /// </exception>
        public static MethodInfo Resolve(Type declaringType, String methodName, params Type[] parameterTypes)
        {
            // Checks whether the declaringType parameter has been supplied
            if (declaringType == null)
            {
                const String DeclaringTypeParameterName = "declaringType";

                throw new ArgumentNullException(DeclaringTypeParameterName);
            }

            // Checks whether the methodName parameter has been supplied
            if (String.IsNullOrEmpty(methodName))
            {
                const String MethodNameParameterName = "methodName";

                throw new ArgumentNullException(MethodNameParameterName);
            }

            MethodCacheKey cacheKey = new MethodCacheKey(declaringType, methodName, parameterTypes);

            return ResolveInternal(_cacheStore, cacheKey);
        }

        /// <summary>
        /// Resolves the internal.
        /// </summary>
        /// <param name="cache">
        /// The cache store.
        /// </param>
        /// <param name="cacheKey">
        /// The cache key.
        /// </param>
        /// <returns>
        /// A <see cref="MethodInfo"/> instance.
        /// </returns>
        private static MethodInfo ResolveInternal(ICacheStore cache, MethodCacheKey cacheKey)
        {
            Object cacheValue = cache[cacheKey.ToString()];

            if (cacheValue != null)
            {
                RuntimeMethodHandle cachedHandle = (RuntimeMethodHandle)cacheValue;

                return MethodBase.GetMethodFromHandle(cachedHandle) as MethodInfo;
            }

            const BindingFlags Flags =
                BindingFlags.ExactBinding | BindingFlags.FlattenHierarchy | BindingFlags.Instance | BindingFlags.Static | BindingFlags.InvokeMethod |
                BindingFlags.Public | BindingFlags.NonPublic;

            // The Type.GetMethod checks for null types so if there are no types provided, this value needs to be an empty array
            if (cacheKey.ParameterTypes == null)
            {
                cacheKey.ParameterTypes = Type.EmptyTypes;
            }

            MethodInfo resolvedMethod = cacheKey.DeclaringType.GetMethod(cacheKey.MethodName, Flags, null, cacheKey.ParameterTypes, null);

            if (resolvedMethod == null)
            {
                // If the declaring type is an interface, the method being sought may not be found as interfaces implement interfaces
                if (cacheKey.DeclaringType.IsInterface)
                {
                    Type[] interfaces = cacheKey.DeclaringType.GetInterfaces();

                    for (Int32 index = 0; index < interfaces.Length; index++)
                    {
                        Type interfaceType = interfaces[index];

                        resolvedMethod = interfaceType.GetMethod(cacheKey.MethodName, Flags, null, cacheKey.ParameterTypes, null);

                        if (resolvedMethod != null)
                        {
                            break;
                        }
                    }
                }
            }

            if (resolvedMethod != null)
            {
                cache[cacheKey.ToString()] = resolvedMethod.MethodHandle;
            }

            return resolvedMethod;
        }
    }
}